﻿/*
 * LINQ to LDAP
 * http://linqtoldap.codeplex.com/
 * 
 * Copyright Alan Hatter (C) 2010-2012
 * alan.hatter@gmail.com - http://hattercoding.blogspot.com/
 * 
 * This project is subject to licensing restrictions. Visit http://linqtoldap.codeplex.com/license for more information.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using LinqToLdap.Exceptions;

namespace LinqToLdap.Mapping
{
    /// <summary>
    /// Defines a mapping for a directory entry. Derive from this class to create a mapping,
    /// and use the constructor to control how your object is queryed.
    /// </summary>
    /// <example>
    /// public class UserMap : ClassMap&lt;User&gt;
    /// {
    ///   public UserMap()
    ///   {
    ///     Map(x => x.Name)
    ///       .Named("displayname");
    ///     Map(x => x.Age);
    ///   }
    /// }
    /// </example>
    /// <typeparam name="T">Type to map</typeparam>
    public abstract partial class ClassMap<T> : IClassMap where T : class 
    {
        private string _namingContext;
        private string _objectCategory;
        private IEnumerable<string> _objectClass;
        private readonly bool _isForAnonymousType;

        /// <summary>
        /// Flags used for looking up properties
        /// </summary>
        protected const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

        /// <summary>
        /// All mapped properties
        /// </summary>
        public readonly List<IPropertyMappingBuilder> PropertyMappings = new List<IPropertyMappingBuilder>();

        /// <summary>
        /// Constructs the class map
        /// </summary>
        protected ClassMap()
        {
            _isForAnonymousType = typeof(T).IsAnonymous();
        }

        /// <summary>
        /// The <see cref="System.Type"/> of the class map.
        /// </summary>
        public Type Type { get { return typeof(T); } }

        /// <summary>
        /// Indicates if the mapping is for an anonymous type.
        /// </summary>
        protected bool IsForAnonymousType { get { return _isForAnonymousType; } }

        /// <summary>
        /// Validates the mapping
        /// </summary>
        /// <exception cref="MappingException">
        /// Thrown if naming context is null or if no properties have been mapped
        /// </exception>
        public void Validate()
        {
            if (PropertyMappings.Count == 0)
                throw new MappingException("At least one property must be mapped.");
        }

        /// <summary>
        /// Gets the naming context
        /// </summary>
        /// <returns></returns>
        protected string GetNamingContext()
        {
            return _namingContext;
        }

        /// <summary>
        /// Gets the object category
        /// </summary>
        /// <returns></returns>
        protected string GetObjectCategory()
        {
            return _objectCategory;
        }

        /// <summary>
        /// Gets the object class
        /// </summary>
        /// <returns></returns>
        protected IEnumerable<string> GetObjectClass()
        {
            return _objectClass;
        }

        /// <summary>
        /// Produces a final mapping used for object contruction from the directory
        /// </summary>
        /// <returns></returns>
        public virtual IObjectMapping ToObjectMapping()
        {
            return new StandardObjectMapping<T>(GetNamingContext(),
                                                PropertyMappings.Select(pmb => pmb.ToPropertyMapping()),
                                                GetObjectCategory(), GetObjectClass());
        }

        /// <summary>
        /// Set the naming context for the directory entry
        /// </summary>
        /// <param name="distinguishedName">The distinguished name of the naming context</param>
        protected void NamingContext(string distinguishedName)
        {
            _namingContext = distinguishedName;
        }

        /// <summary>
        /// Set the object category for the directory entry
        /// </summary>
        /// <param name="objectCategory">The object category</param>
        protected void ObjectCategory(string objectCategory)
        {
            _objectCategory = objectCategory;
        }

        /// <summary>
        /// Set the object classes for the directory entry
        /// </summary>
        /// <param name="objectClass">The object class</param>
        protected void ObjectClasses(IEnumerable<string> objectClass)
        {
            _objectClass = objectClass;
        }

        /// <summary>
        /// Set the object class for the directory entry
        /// </summary>
        /// <param name="objectClass">The object class</param>
        protected void ObjectClass(string objectClass)
        {
            if (!objectClass.IsNullOrEmpty())
            {
                _objectClass = new List<string>(1) { objectClass };
            }
        }

        /// <summary>
        /// Maps the <paramref name="property"/> as the distinguished name.
        /// </summary>
        /// <param name="property">The distinguished name property.</param>
        /// <param name="attributeName">The name of the distinguished name attribute. Defaults to "distinguishedname".</param>
        protected void DistinguishedName(Expression<Func<T, string>> property, string attributeName = "distinguishedname")
        {
            if (PropertyMappings.Any(p => p.IsDistinguishedName))
                throw new MappingException("Cannot specify more than one DistinguishedName property.");

            var propertyInfo = GetPropertyInfo(property.Body);
            DistinguishedName(propertyInfo, attributeName);
        }

        internal void DistinguishedName(PropertyInfo property, string attributeName = "distinguishedname")
        {
            if (PropertyMappings.Any(p => p.IsDistinguishedName))
                throw new MappingException("Cannot specify more than one DistinguishedName property.");
            if (string.IsNullOrWhiteSpace(attributeName)) throw new MappingException("DistinguishedName must have an attribute name.");

            Map<string>(property, true).Named(attributeName);
        }
        
        private PropertyInfo GetPropertyInfo(Expression expression)
        {
            if (!(expression is MemberExpression))
                throw new ArgumentException("Expected MemberAccess expression but was " + expression.NodeType);

            var member = (expression as MemberExpression).Member;

            if (PropertyMappings.Any(p => p.PropertyName == member.Name))
            {
                throw new MappingException(string.Format("{0} is already mapped for {1}", member.Name,
                                                         typeof(T).FullName));
            }

            var propertyInfo = typeof(T).GetProperty(member.Name, Flags);
            if (propertyInfo == null)
            {
                throw new MappingException(string.Format("Property named {0} not found for type {1}", member.Name,
                                                         typeof(T).FullName));
            }
            if (propertyInfo.GetSetMethod() == null || propertyInfo.GetGetMethod() == null)
            {
                throw new MappingException("Cannot map a property without a getter and setter.");
            }

            return propertyInfo;
        }

        internal IPropertyMapperGeneric<TProperty> Map<TProperty>(PropertyInfo propertyInfo, bool isDistinguishedName = false, bool isReadOnly = false)
        {
            if (propertyInfo == null) throw new ArgumentNullException("propertyInfo");

            if (isDistinguishedName && PropertyMappings.Any(p => p.IsDistinguishedName))
                throw new MappingException("Cannot specify more than one DistinguishedName property.");

            var mapping = new PropertyMappingBuilder<T, TProperty>(propertyInfo, isDistinguishedName, isReadOnly);

            PropertyMappings.Add(mapping);

            return mapping;
        }

        internal TBuilder Map<TBuilder>(TBuilder builder) where TBuilder : IPropertyMappingBuilder
        {
            if (builder.PropertyInfo == null) throw new ArgumentNullException("builder");

            if (builder.IsDistinguishedName && PropertyMappings.Any(p => p.IsDistinguishedName))
                throw new MappingException("Cannot specify more than one DistinguishedName property.");

            PropertyMappings.Add(builder);

            return builder;
        }

        internal IPropertyMapper MapNonGeneric(PropertyInfo propertyInfo, bool isDistinguishedName = false, bool isReadOnly = false)
        {
            if (propertyInfo == null) throw new ArgumentNullException("propertyInfo");

            if (isDistinguishedName && PropertyMappings.Any(p => p.IsDistinguishedName))
                throw new MappingException("Cannot specify more than one DistinguishedName property.");

            if (isReadOnly && PropertyMappings.Any(p => p.IsReadOnly))
                throw new MappingException("Cannot specify more than one CommonName property.");

            var type = typeof (PropertyMappingBuilder<,>).MakeGenericType(typeof (T), propertyInfo.PropertyType);

            var mapping = Activator.CreateInstance(type, new object[] {propertyInfo, isDistinguishedName, isReadOnly}) as IPropertyMappingBuilder;

            PropertyMappings.Add(mapping);

            return mapping as IPropertyMapper;
        }

        /// <summary>
        /// Create a custom property mapping.
        /// </summary>
        /// <param name="property">Property to map</param>
        /// <example>
        /// MapCustom(x => x.Name);
        /// </example>
        protected ICustomPropertyMapper<T, TProperty> MapCustom<TProperty>(Expression<Func<T, TProperty>> property)
        {
            var propertyInfo = GetPropertyInfo(property.Body);

            var mapping = new CustomPropertyMappingBuilder<T, TProperty>(propertyInfo);

            PropertyMappings.Add(mapping);

            return mapping;
        }
    }
}
